#!/bin/env python

"""This module describes how Dingo represents individual sensors.

Copyright (C) Sarah Mount, 2005-2008.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA


Sensor objects describe the state and operations of an individual
sensor. Sensor object will not usually run in the same process as
their corresponding SensorAvatar object and so must NEVER hold a weak
or strong reference to that object. Instead, they send pub/sub
messages which GUIs will subscribe to and handle.
"""

import Dingo.scheduler as scheduler
import Dingo.locks as locks
import Dingo.consts as consts
import Dingo.OGL as OGL
from Dingo.pubsub import Publisher

from Queue import Queue
import socket, sys, time, threading, wx
import pdb

__author__  = "Sarah Mount <s.mount@wlv.ac.uk>"
__credits__ = "Daniel Goldsmith"
__date__    = "2005/01/07 15:11:19"

DEBUG = 1


class Sensor(threading.Thread):
    """Superclass for sensor implementations.
    
    Think of sensors as a blank piece of memory. Each sensor runs in it's
    own thread and so each named chunk of memory needs to be locked and
    unlocked whenever it's accessed or modified. Subclasses of Sensor
    should add whatever attributes are needed to achieve this.

    In a Simulation, each Sensor has a Scheduler with a Task list where
    each Task has a reference to it's Sensor (so it can read/write to that
    Sensors memory and call send() and recv() in order to communicate).
    """
    CLK_DUMMY = "CLK_DUMMY" # Used by par()
    """@cvar: Used by L{Dingo.sensor.Sensor.par to fake parallelism}"""

    def __init__(self, id, coords, colour, scheduler, ether, router, size=25,shape=None):
        """Initialise a new Sensor Object.
        
        This will create a new sensor object in the simulation, each
        sensor has its own unique ID that is used to identify it
        within the simulation.

        @type coords: tuple
        @param coords: A tuple (x,y) containing the sensors coordinates on the graph pane
        @type colour: Dingo.OGL.SensorColour
        @param colour: An object denoting the colour initially used to display the sensor in the graph
        @type scheduler: Dingo.scheduler.Scheduler
        @param scheduler: The scheduler describing the run-time behaviour of the sensor
        @type ether: Dingo.simulation.Ether
        @param ether: A reference to the simulated transmission medium used by this sensor
        """
        self.id = id # Sensor identification
        threading.Thread.__init__(self, name=self.id)
        self.setDaemon(True)
	self.coords = coords
        self.colour = colour
        self.size = size
        self.shape = shape
        self.createAvatar()
        self.scheduler = scheduler # Simulates an OS scheduler
	self.router = router
        self.hasGUI = True
        """ivar: C{True} if a GUI is attached and C{False} otherwise."""
        self.recv_q = Queue() ### WHERE SHOULD THIS GO?
        """ivar: Thread-safe queue to hold incoming messages of type Packet"""
        self.send_q = Queue()
    	"""ivar: Thread-safe queue to hold outgoing messages of type Packet"""
        self.io_q = Queue()
    	"""ivar: Thread-safe queue to hold I/O events within or this sensor"""
        self.ether = ether # TODO: remove
	self.setRouter(self.router)
        # Subscribe to pubsub topics
	Publisher().subscribe(self.onAttachGUI, (consts.ATTACH_TOPIC))	
	Publisher().subscribe(self.onDetachGUI, (consts.DETACH_TOPIC))	
	Publisher().subscribe(self.onMove,      (consts.MOVE_TOPIC))	
        return

    def setRouter(self, router):
	"""Ensure the router has access to send and recv queues.
	"""
	self.router = router
	self.router.send_q = self.send_q
	self.router.recv_q = self.recv_q
	self.router.radio = self.ether
	self.router.sensor_id = self.id
	return

    def debug(self, message):
        """Send a debug message to the Debug plugin.
	See also L{Dingo.plugins.debug}

	@type message: str
        @param message: Text to send to the debug plugin
        """
        consts.debug(message)
        return

    def createAvatar(self):
	# Create new avatar in GUI
        msg = {'id':self.id, 'coords':self.coords, 'colour':self.colour, 'size':self.size, 'shape':self.shape}
        Publisher().sendMessage((consts.NEW_AVATAR_TOPIC), msg)
        return

    def onMove(self, msg):
        if msg.data['id'] == self.id:
            self.coords = msg.data['coords']
            print self.id, 'moved to', self.coords
        return

    def onAttachGUI(self, msg):
        self.hasGUI = True
        self.createAvatar()
        return

    def onDetachGUI(self, msg):
        self.hasGUI = False
        return

    def setShape(self, newShape = None):
        """Change the shape of the graphical representation of this sensor.
        Contributed by Daniel Goldsmith.

	@precondition: C{newShape in ['circle', 'square', 'triangle'] or type(newShape) == type([])}
        @type newshape: str
        @param newshape: Shape to represent this sensor. 
        @type size: int
        @param size: size of new shape
        """
        self.shape = newShape
        if self.hasGUI:
            Publisher().sendMessage((consts.SHAPE_TOPIC), {'id':self.id, 'shape':newShape})
        return

    def setColour(self, colour):
        """Change the colour of the graphical representation of this sensor.

        @type colour: SensorColour
        @param colour: colour to set the avatar of this sensor
        """
        self.colour = colour
        if self.hasGUI:
            Publisher().sendMessage((consts.COLOUR_TOPIC), {'id':self.id, 'colour':colour})
        return

    def clock(self):
        """Report the current time (as a float).
        Time is taken from the processor and should be accurate to
        the nearest millisecond.
        To simulate clock drift; override this method and add a
        random value to the result of time.clock().

        @rtype: float
        """
        return time.clock()

    def send(self, arc_colour):	return self.router.send(arc_colour)

    def sendAll(self, arc_colour): return self.router.sendAll(arc_colour)

    def recv(self): return self.router.recv()

    
#     def send(self, arc_colour):
#         """Send a message to another Sensor.
#         This will take whatever messages are currently in the sensors
#         send Queue forward them to the sensor specified

#     	@type arc_colour: Dingo.OGL.SensorColour
#         @param arc_colour: colour arc is used on the sensorGraph
        
#         B{BUG:} This method ignores the broadcast range of the radio,
#         messages can therefore be sent further than is possible in the
#         medium
#         """
#         if not self.send_q.empty():
#             msg = self.send_q.get()
#             msg.time_sent = self.clock()
#             self.ether.send(msg, arc_colour)
#             return
#         return
    
#     def recv(self):
#         """Recv a message from another Sensor, via the transimssion medium.
#         This will return the top item in the sensors receive queue.

#         @rtype: object
#         """
#         if not self.recv_q.empty():
#             return self.recv_q.get()
            
#     def broadcast(self, arc_colour, time_sent=None, msg=None):
#         """Broadcast messages to all sensors with a given radius.
#         Broadcast radius is set in api.Ether.RF_radius and is
#         measured in pixels, each of which represents one
#         square centimetre.

#         @type arc_colour: Dingo.OGL.SensorColour
#         @param arc_colour: Colour of the arc used to represent this broadcast.
#         @type time_sent: float
#         @param time_sent: Can be used to change the sending time of the message
#         @type msg: Dingo.packet.Packet
#         @para msg: The message to send, this is generally a packet object        
#         """
#         msg.time_sent = self.clock()
#         self.ether.broadcast(self.id, arc_colour, time_sent, msg)
#         return

#     def poolBC(self, arc_colour, time_sent=None, msg=None):
#         """Contributed by Daniel Goldsmith."""
#         self.ether.poolBC(self.id, arc_colour, time_sent, msg)

#     def multicast(self, arc_colour, time_sent=None, msg=None):
#         """Multicast messages to all specified sensors.
#         This allows the sensor to multicast messages to a list of
#         receivers.  The destination address are taken from the
#         msg.receiver_id field and should be input as a list / tuple.
        
#         Sensors in the receiver list are checked to see if they are in
#         broadcast range.  L{Dingo.simulation.Ether.broadcast}broadcast
#         has a detailed explanation of how the range is calculated

# 	Contributed by Daniel Goldsmith.

#         @type arc_colour: Dingo.OGL.SensorColour
#         @param arc_colour: What colour arc to draw on the sensorGraph
#         @type time_sent: float
#         @param time_sent What time the message was sent
#         @type msg: Dingo.packet.Packet
#         @param msg: The message to send this should be a packet object
#         """
#         msg.time_sent = self.clock()
#         self.ether.multicast(self.id, arc_colour, time_sent, msg)
#         return

#     def testPool(self,arc_colour): # What was this for?!
#         if not self.send_q.empty():
#             msg = self.send_q.get()
#             self.ether.testPool(msg,arc_colour)
#             return "Msg"
#         return "Empty"

    def par(self, *calls):
        """Execute several method calls at the same time.
        The argument to par should be a list of
        (method-object, args-tuple) 2-tuples.
        If Sensor.CLK_DUMMY appears in any argument list it will
        be replaced with a timestamp when the method is called.
        """
        timestamp = self.clock()
        def subst(anything, time):
            if anything == Sensor.CLK_DUMMY:
                return time
            else:
                return anything
        def subst_time(arg_tuple, time):
            return tuple(map(lambda x : subst(x, time), arg_tuple))
        for i in calls:
#           print "i", i
            (method_obj, args) = i
#           print "method_obj", method_obj
#           print "args", args
            new_args = subst_time(args, timestamp)
#           print "new_args", new_args
            method_obj(*new_args)

    def run(self):
        """This method is called by the UI when a simulation is run.
        The run method will call the scheduler to see which tasks are
        to be completed then preform these tasks.
        """
	if not self.scheduler.canRun():
	    print "Finished Execution at %s" %time.clock()
        try:
            self.scheduler.run()
        except locks.SimulationCanceled:
            pass
        except OGL.DeletedException:
            pass
        except:
            try:
                t, exc, tb = sys.exc_info()
                #sys.excepthook(t, exc, tb)
                if not OGL.SensorGraph.Instance._app.dying:
                    wx.CallAfter(wx.MessageBox, `(exc,)+exc.args`, "Dingo", wx.ICON_ERROR)
            except:
                pass

    def __repr__(self):
        """Return a formatted string representing the sensor object."""
        return 'Sensor(' + self.id + ')'

if __name__ == '__main__': # Only for unit testing
    pass
